using System;
using System.ComponentModel;
using System.Collections;
using System.Data;
using System.Drawing;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace MapMaker.VirtualEarth
{
    public class VirtualEarthTileAccess
    {
        private const int MAX_QUEUE_SIZE = 1000;

        private Queue tileQueue = new Queue();
        private ArrayList tilesToDownload;
        private ArrayList zoomExceptions;
        private bool endOfQueue = false;
        private int topLeftTileX;
        private int topLeftTileY;
        private int bottomRightTileX;
        private int bottomRightTileY;
        private string typeKey;
        private string fileExtension;
        private int levelOfDetail;
        private MapType mapType;
        private int numDownloaded = 0;

        public delegate void GotFileHandler(int filesDownloaded);
        public delegate void GotMapHandler();

        public event GotFileHandler GotFile;
        public event GotMapHandler GotMap;
        public static event GotFileHandler GotPreviewImage;

        public string MapName;
        public string SaveDirectory;

        public const int MAP_VIEW_SIZE = 512;
        public const int PREVIEW_STEP = 1;

        public enum MapType
        {
            Road,
            Hybrid,
            Aerial
        }

        public ArrayList TilesToDownload
        {
            get
            {
                if (tilesToDownload == null)
                {
                    tilesToDownload = new ArrayList();
                }
                return tilesToDownload;
            }
        }

        public ArrayList ZoomExceptions
        {
            get
            {
                if (zoomExceptions == null)
                {
                    zoomExceptions = new ArrayList();
                }
                return zoomExceptions;
            }
        }

        public int DownloadTiles(int centerPixelX, int centerPixelY, int levelOfDetail, MapType mapType)
        {
            // create a new thread to run this operation
            Program.WorkerThread = new Thread(new ThreadStart(enqueueTiles));
            Program.PrimaryDownloadWorker = new Thread(new ThreadStart(startDownload));
            Program.SecondaryDownloadWorker = new Thread(new ThreadStart(startDownload));
            
            DirectoryInfo dir = null;

            int tileCount = GetTileCount(centerPixelX, centerPixelY, levelOfDetail, mapType);

            SaveDirectory = Path.Combine(SaveDirectory, MapName);

            dir = new DirectoryInfo(SaveDirectory);

            // check if the directory exists
            if (!dir.Exists)
            {
                dir.Create();
            }
            else
            {
                if (dir.GetDirectories() != null || dir.GetFiles() != null)
                {
                    foreach (FileInfo file in dir.GetFiles())
                    {
                        file.Delete();
                    }

                    foreach (FileInfo directory in dir.GetFiles())
                    {
                        directory.Delete();
                    }
                }
            }

            // set the map view type
            this.mapType = mapType;

            // set the lowest level of detail in view
            this.levelOfDetail = levelOfDetail;

            // check the map view type and set the type key and file extension
            switch (mapType)
            {
                case MapType.Aerial:
                    fileExtension = ".jpg";
                    typeKey = "a";
                    break;
                case MapType.Hybrid:
                    fileExtension = ".jpg";
                    typeKey = "h";
                    break;
                case MapType.Road:
                    fileExtension = ".png";
                    typeKey = "r";
                    break;
            }

            // start the tile queue thread
            Program.WorkerThread.Start();

            if (tileCount < MAX_QUEUE_SIZE)
            {
                while (tileQueue.Count < tileCount)
                {
                    Thread.Sleep(200);
                }
            }
            else
            {
                while (tileQueue.Count < MAX_QUEUE_SIZE)
                {
                    Thread.Sleep(200);
                }
            }

            // start the download thread
            Program.PrimaryDownloadWorker.Start();
            Program.SecondaryDownloadWorker.Start();

            return tileCount;
        }

        public int GetTileCount(int centerPixelX, int centerPixelY, int levelOfDetail, MapType mapType)
        {
            int previewStep = PREVIEW_STEP;
            int numTiles = 0;
            int previewDetail; 
            int tileWidth = 0;
            int lowerX = 0;
            int upperX = 0;
            int lowerY = 0;
            int upperY = 0;

            this.levelOfDetail = levelOfDetail;

            if (tilesToDownload == null)
            {
                Program.TileAccess.GetTilePreviewList(
                    centerPixelX,
                    centerPixelY,
                    levelOfDetail, mapType, false);
            }

            if (levelOfDetail == 18)
            {
                previewStep = 0;
            }
            else if (levelOfDetail == 17)
            {
                previewStep = 1;
            }

            previewDetail = levelOfDetail + previewStep;

            // check the map view type and set the type key
            switch (mapType)
            {
                case MapType.Road:
                    typeKey = "r";
                    break;
                case MapType.Hybrid:
                    typeKey = "h";
                    break;
                case MapType.Aerial:
                    typeKey = "a";
                    break;
            }

            // set the corner pixel values for this detail level
            VirtualEarthTileSystem.PixelXYToTileXY(
                centerPixelX - (VirtualEarthTileAccess.MAP_VIEW_SIZE / 2) - 1,
                centerPixelY - (VirtualEarthTileAccess.MAP_VIEW_SIZE / 2) - 1, 
                out topLeftTileX, out topLeftTileY);
            VirtualEarthTileSystem.PixelXYToTileXY(
                centerPixelX + (VirtualEarthTileAccess.MAP_VIEW_SIZE / 2) - 1,
                centerPixelY + (VirtualEarthTileAccess.MAP_VIEW_SIZE / 2) - 1, 
                out bottomRightTileX, out bottomRightTileY);

            for (int tileY = topLeftTileY; tileY <= bottomRightTileY; tileY++)
            {
                for (int tileX = topLeftTileX; tileX <= bottomRightTileX; tileX++)
                {
                    for (int i = 0; i < previewStep; i++)
                    {
                        tileWidth = (int)Math.Pow(2, i);
                        lowerX = (int)Math.Floor(tileX * Math.Pow(2, i));
                        upperX = lowerX + tileWidth;
                        lowerY = (int)Math.Floor(tileY * Math.Pow(2, i));
                        upperY = lowerY + tileWidth;

                        // loop through the rows
                        for (int y = lowerY; y < upperY; y++)
                        {
                            // loop through the columns
                            for (int x = lowerX; x < upperX; x++)
                            {
                                numTiles++; //increment the tile count
                            }
                        }
                    }
                }
            }

            foreach (Tile tile in tilesToDownload)
            {
                // loop through levels of detail
                for (int i = 0; i < 18 - tile.LevelOfDetail; i++)
                {
                    if (!ZoomExceptions.Contains((int)(i + tile.LevelOfDetail)))
                    {
                        tileWidth = (int)Math.Pow(2, i);
                        lowerX = (int)Math.Floor(tile.X * Math.Pow(2, i));
                        upperX = lowerX + tileWidth;
                        lowerY = (int)Math.Floor(tile.Y * Math.Pow(2, i));
                        upperY = lowerY + tileWidth;

                        // loop through the rows
                        for (int y = lowerY; y < upperY; y++)
                        {
                            // loop through the columns
                            for (int x = lowerX; x < upperX; x++)
                            {
                                numTiles++; //increment the tile count
                            }
                        }
                    }
                }
            }

            return numTiles; // return the title count
        }

        private void startDownload()
        {
            Tile tile = null;
            string quadKey = null;
            string savePath = null;
            string url = null;
            FileInfo file = null;

            Random randNum = new Random();
            WebClient downloadClient = new WebClient();

            try
            {
                while (tileQueue.Count > 0 || !endOfQueue)
                {
                    if (tileQueue.Count <= 50 && !endOfQueue)
                    {
                        Thread.Sleep(100);
                    }
                    else
                    {
                        // get the next tile to download
                        tile = (Tile)tileQueue.Dequeue();

                        // convert tile coordinates to quad key
                        quadKey = VirtualEarthTileSystem.TileXYToQuadKey(tile.X, tile.Y, tile.LevelOfDetail);

                        // get tile save path
                        savePath = Path.Combine(Path.Combine(SaveDirectory, tile.LevelOfDetail.ToString()), tile.Y.ToString());

                        // check if detail level directory exists
                        if (!Directory.Exists(Path.Combine(SaveDirectory, tile.LevelOfDetail.ToString())))
                        {
                            // create detail level directory
                            Directory.CreateDirectory(Path.Combine(SaveDirectory, tile.LevelOfDetail.ToString()));
                        }

                        // check if save directory exisits
                        if (!Directory.Exists(savePath))
                        {
                            // create save directory
                            Directory.CreateDirectory(savePath);
                        }

                        // create tile download URL
                        url = string.Concat("http://r", randNum.Next(0, 3).ToString(),
                                ".ortho.tiles.virtualearth.net/tiles/",
                                typeKey, quadKey, fileExtension, "?g=60");

                        try
                        {
                            // download tile image
                            downloadClient.DownloadFile(url,
                                Path.Combine(savePath, (tile.X.ToString() + fileExtension)));

                            file = new FileInfo(Path.Combine(savePath, (tile.X.ToString() + fileExtension)));

                            // check if the file was saved
                            if (file.Exists)
                            {
                                // check if the save file has any data in it
                                if (file.Length > 0)
                                {
                                    // increment tile count
                                    numDownloaded++;

                                    if (GotFile != null)
                                    {
                                        // raise got file event
                                        GotFile(numDownloaded);
                                    }
                                }
                                else
                                {
                                    // delete the file put it back in the queue for re-download
                                    file.Delete();
                                    tileQueue.Enqueue(tile);
                                }
                            }
                            else
                            {
                                // put tile back into the queue for re-download
                                tileQueue.Enqueue(tile);
                            }
                        }
                        catch (WebException ex)
                        {
                            // check for protocol error
                            if (ex.Status == WebExceptionStatus.ProtocolError)
                            {
                                // most likely isolated error, 
                                // put tile back into queue for another attempt
                                tileQueue.Enqueue(tile);
                            }
                        }
                    }

                    if (endOfQueue && tileQueue.Count == 0)
                    {
                        // raise got map event
                        GotMap();
                    }

                    // update user interface
                    Application.DoEvents();
                }
            }
            catch (ThreadAbortException)
            { }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void enqueueTiles()
        {
            int previewStep = PREVIEW_STEP;
            int previewDetail;
            int tileWidth = 0;
            int lowerX = 0;
            int upperX = 0;
            int lowerY = 0;
            int upperY = 0;

            tileQueue = new Queue();
            endOfQueue = false;

            if (levelOfDetail == 18)
            {
                previewStep = 0;
            }
            else if (levelOfDetail == 17)
            {
                previewStep = 1;
            }

            previewDetail = levelOfDetail + previewStep;

            try
            {
                for (int tileY = topLeftTileY; tileY <= bottomRightTileY; tileY++)
                {
                    for (int tileX = topLeftTileX; tileX <= bottomRightTileX; tileX++)
                    {
                        for (int i = 0; i < previewStep; i++)
                        {
                            tileWidth = (int)Math.Pow(2, i);
                            lowerX = (int)Math.Floor(tileX * Math.Pow(2, i));
                            upperX = lowerX + tileWidth;
                            lowerY = (int)Math.Floor(tileY * Math.Pow(2, i));
                            upperY = lowerY + tileWidth;

                            // loop through the rows
                            for (int y = lowerY; y < upperY; y++)
                            {
                                // loop through the columns
                                for (int x = lowerX; x < upperX; x++)
                                {
                                    while (tileQueue.Count >= MAX_QUEUE_SIZE)
                                    {
                                        Thread.Sleep(10);
                                    }

                                    tileQueue.Enqueue(new Tile(x, y, levelOfDetail + i));
                                }
                            }
                        }
                    }
                }

                foreach (Tile tile in tilesToDownload)
                {
                    // loop through levels of detail
                    for (int i = 0; i < 18 - tile.LevelOfDetail; i++)
                    {
                        if (!ZoomExceptions.Contains((int)(i + tile.LevelOfDetail)))
                        {
                            tileWidth = (int)Math.Pow(2, (i));
                            lowerX = tile.X * (int)Math.Pow(2, i);
                            upperX = lowerX + tileWidth;
                            lowerY = tile.Y * (int)Math.Pow(2, i);
                            upperY = lowerY + tileWidth;

                            // loop through the rows
                            for (int y = lowerY; y < upperY; y++)
                            {
                                // loop through the columns
                                for (int x = lowerX; x < upperX; x++)
                                {
                                    while (tileQueue.Count >= MAX_QUEUE_SIZE)
                                    {
                                        Thread.Sleep(10);
                                    }

                                    tileQueue.Enqueue(new Tile(x, y, tile.LevelOfDetail + i));
                                }
                            }
                        }
                    }
                }

                endOfQueue = true;
            }
            catch (ThreadAbortException)
            { }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        public Queue GetTilePreviewList(int centerPixelX, int centerPixelY, int levelOfDetail, MapType mapType, bool getImages)
        {
            Queue previewList = new Queue();
            int previewStep = PREVIEW_STEP;
            int previewDetail = 0;
            string quadKey = null;
            PictureBox tilePreview = null;
            Bitmap tileImage = null;
            Random randNum = new Random();
            string fileExtension = null;
            string typeKey = null;
            string url = null;
            int xOffset = 0;
            int yOffset = 0;
            int previewWidth = 96;
            int count = 0;
            WebClient wc = new WebClient();
            Tile tile = null;

            TilesToDownload.Clear();

            switch (mapType)
            {
                case VirtualEarthTileAccess.MapType.Aerial:
                    fileExtension = ".jpg";
                    typeKey = "a";
                    break;
                case VirtualEarthTileAccess.MapType.Hybrid:
                    fileExtension = ".jpg";
                    typeKey = "h";
                    break;
                case VirtualEarthTileAccess.MapType.Road:
                    fileExtension = ".png";
                    typeKey = "r";
                    break;
            }

            if (levelOfDetail == 18)
            {
                previewStep = 0;
            }
            else if (levelOfDetail == 17)
            {
                previewStep = 1;
            }

            previewDetail = levelOfDetail + previewStep;

            int topLeftPixelX = centerPixelX - (MAP_VIEW_SIZE - 1);
            int topLeftPixelY = centerPixelY - (MAP_VIEW_SIZE - 1);

            int previewTileWidth = ((MAP_VIEW_SIZE + 256) / 256) * (int)Math.Pow(2, previewStep);
            int topLeftTileX;
            int topLeftTileY;

            // set the corner tile values for this detail level
            VirtualEarthTileSystem.PixelXYToTileXY((topLeftPixelX * (int)Math.Pow(2, previewStep)), (topLeftPixelY * (int)Math.Pow(2, previewStep)), out topLeftTileX, out topLeftTileY);
            
            // loop through the rows
            for (int y = topLeftTileY + 1; y <= topLeftTileY + previewTileWidth; y++)
            {
                // loop through the columns
                for (int x = topLeftTileX + 1; x <= topLeftTileX + previewTileWidth; x++)
                {
                    tile = new Tile(x, y, previewDetail);

                    if (getImages)
                    {
                        quadKey = VirtualEarthTileSystem.TileXYToQuadKey(x, y, previewDetail);

                        url = "http://" + typeKey + randNum.Next(0, 3) +
                            ".ortho.tiles.virtualearth.net/tiles/" +
                            typeKey + quadKey + fileExtension + "?g=60";

                        tilePreview = new PictureBox();
                        tilePreview.Location = new System.Drawing.Point(xOffset, yOffset);
                        tilePreview.Size = new System.Drawing.Size(previewWidth, previewWidth);
                        tilePreview.BorderStyle = BorderStyle.FixedSingle;
                        tilePreview.SizeMode = PictureBoxSizeMode.StretchImage;

                        tilePreview.Tag = tile;

                        while (tileImage == null)
                        {
                            try
                            {
                                tileImage = new Bitmap(wc.OpenRead(url));
                            }
                            catch (WebException ex)
                            {
                                if (ex.Status != WebExceptionStatus.ProtocolError)
                                {
                                    throw new Exception(ex.Message);
                                }
                            }
                        }

                        tilePreview.BackgroundImageLayout = ImageLayout.Stretch;
                        tilePreview.BackgroundImage = tileImage;
                        
                        previewList.Enqueue(tilePreview);

                        tileImage = null;
                    }

                    TilesToDownload.Add(tile);

                    xOffset += previewWidth;

                    count++;

                    if (GotPreviewImage != null)
                    {
                        GotPreviewImage(count);
                    }
                }
                
                xOffset = 0;
                yOffset += previewWidth;

                Application.DoEvents();
            }

            System.GC.Collect();

            return previewList;
        }

        public static int GetTilePreviewCount(int centerPixelX, int centerPixelY, int levelOfDetail)
        {
            int previewStep = PREVIEW_STEP;

            if (levelOfDetail == 18)
            {
                previewStep = 0;
            }
            else if (levelOfDetail == 17)
            {
                previewStep = 1;
            }

            return (int)Math.Pow(((MAP_VIEW_SIZE + 256) / 256) * (int)Math.Pow(2, previewStep), 2);
        }
    }
}
